﻿/*
 * SshIO.java
 *
 * Created on 2007年7月19日, 上午 11:11
 */

package org.uranus.core.ssh;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Random;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import org.uranus.core.dataHandler.HostInfoData;
import org.uranus.core.ssh.v1.MD5;
import org.uranus.core.ssh.v2.AlgAnalysis;
import org.uranus.core.ssh.v2.DHKex;
import org.uranus.core.ssh.v2.HMACSHA1;
import org.uranus.core.ssh.v2.SHA1;
import org.uranus.core.ssh.v2.SshPacketV2;
import org.uranus.core.ssh.v2.SshCrypto2;
import org.uranus.core.terminal.ANSI;
import org.uranus.core.terminal.Terminal;
import org.uranus.mainGUI.GUI;



/**
 * Copyright 2007 Hsiang-Yu Wang<br>
 * This file is distributed under the terms of the GNU General Public License<br>
 *<br>
 *<pre>
 * --------------------License notice--------------------
 * GNU GENERAL PUBLIC LICENSE
 * Version 3, 29 June 2007
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not,
 * visit: http://www.gnu.org/licenses/gpl.txt
 * --------------------License notice--------------------</pre>
 * @author Hsiang-Yu Wang
 */
public class SshIO extends Thread{
    private StreamConnection sc;
    private OutputStream os;
    private InputStream is;
    
    private byte[] session_id;
    
    private SshCrypto2 crypto2;
    
    private ANSI buf;
    private GUI gui;
    
    private int outgoingSeq = 0;
    
    private final int SSH_MSG_DISCONNECT = 1;
    private final int SSH_MSH_IGNORE = 2;
    private final int SSH_MSG_UNIMPLEMENTED = 3;
    private final int SSH_MSG_DEBUG = 4;
    private final int SSH_MSG_SERVICE_REQUEST = 5;
    private final int SSH_MSG_SERVICE_ACCEPT = 6;
    private final int SSH_MSG_KEXINIT = 20;
    private final int SSH_MSG_NEWKEYS = 21;
    private final int SSH_MSG_KEXDH_INIT = 30;
    private final int SSH_MSG_KEXDH_REPLY = 31;
    private final int SSH_MSG_USERAUTH_REQUEST = 50;
    private final int SSH_MSG_USERAUTH_FAILURE = 51;
    private final int SSH_MSG_USERAUTH_SUCCESS = 52;
    private final int SSH_MSG_USERAUTH_BANNER = 53;
    private final int SSH_MSG_USERAUTH_INFO_REQUEST = 60;
    private final int SSH_MSG_GLOBAL_REQUEST = 80;
    private final int SSH_MSG_REQUEST_SUCCESS = 81;
    private final int SSH_MSG_REQUEST_FAILURE = 82;
    private final int SSH_MSG_CHANNEL_OPEN = 90;
    private final int SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91;
    private final int SSH_MSG_CHANNEL_OPEN_FAILURE = 92;
    private final int SSH_MSG_CHANNEL_WINDOW_ADJUST = 93;
    private final int SSH_MSG_CHANNEL_DATA = 94;
    private final int SSH_MSG_CHANNEL_EXTENDED_DATA = 95;
    private final int SSH_MSG_CHANNEL_EOF = 96;
    private final int SSH_MSG_CHANNEL_CLOSE = 97;
    private final int SSH_MSG_CHANNEL_REQUEST = 98;
    private final int SSH_MSG_CHANNEL_SUCCESS = 99;
    private final int SSH_MSG_CHANNEL_FAILURE = 100;
    
    private String idstr;
    private String idstr_c;
    private DHKex dhkex;
    private Terminal terminal;
    
    
    private byte[] orphan;
    
    
    /** @param authMode:       -1:publicKey
     *                          0:password
     *                          1:keyboard-interactive
     */
    private byte authMode = 0;
    
    
    /** <pre>
     *  state -1: error occured.
     *         0: init.
     *         1: init-version check accomplished
     *         2:
     *  </pre>
     */
    private int state;
    
    private HostInfoData info;
    
    private int inInt;
    /**
     * Creates a new instance of SshIO
     */
    public SshIO(HostInfoData info) {
        this.info = info;
        inInt = 0;
        state = 0;
        buf = this.getAnsi();
        
    }
    private void print(String out){
        buf.pushChars(out.toCharArray());
        buf.clearBuffer();
    }
    public void run(){
        try {
            terminal = GUI.getCurrentTerminal();
            sshInit();
        } catch (IllegalArgumentException ex) {
            print(ex.getMessage());
            ex.printStackTrace();
        } catch (SSHConnectionException ex) {
            print(ex.getMessage());
            ex.printStackTrace();
        } catch (IOException ex) {
            print(ex.getMessage());
            ex.printStackTrace();
        } catch (Exception ex) {
            print(ex.getMessage());
            ex.printStackTrace();
        } finally{
            try {
                if(connectionAlive())
                    disconnect();
            } catch (IOException ex) {
                ex.printStackTrace();
            } catch(Exception ex){
                ex.printStackTrace();
            }
        }
    }
    
    public ANSI getAnsi(){
        if( buf == null){
            buf = new ANSI();
            buf.setModeSSH();
        }
        return buf;
    }
    private SshPacketV2 encryptedPacketRcv() throws IOException, InterruptedException{
        byte[] in = new byte[0];
        if(orphan == null){
            while(is.available() == 0){
                Thread.sleep(50);
            }
            in = new byte[is.available()];
            for(int i = 0 ; i < in.length ; i++){
                in[i] = (byte)dataRcv();
            }
        }else{
            in = orphan;
        }
        
        int mlen = crypto2.getRcvHmac().getBlockSize();
        int wholePacket_length = in.length;
        int offset = 0;
        int packet_length = 0;
        SshPacketV2 p;
        
        byte[] head = new byte[8]; // packet length, padding length, msg code , and 2 payload's elements
        System.arraycopy(in,0,head,0,8);
        head = crypto2.decrypt(head);
        packet_length = ((head[0] & 0xFF) << 24) + ((head[1] & 0xFF) << 16) + ((head[2] & 0xFF) << 8) + (head[3] & 0xFF);
        
        byte[] block = new byte[packet_length - 4]; //3 bytes and padding length are already decrypted
        System.arraycopy(in,8,block,0,block.length);
        block = crypto2.decrypt(block);
        
        offset = packet_length + 4 + mlen;
        
        // 把剩下的內容丟到orphan array中, 等待下次呼叫時使用
        if(offset < in.length){
            orphan = new byte[wholePacket_length - offset];
            System.arraycopy(in,offset,orphan,0, wholePacket_length - offset);
        }else{
            orphan = null;
        }
        
        // combine
        byte[] plainText = new byte[head.length + block.length];
        System.arraycopy(head,0,plainText,0,8);
        System.arraycopy(block,0,plainText,8,block.length);
        
        offset = 0 ;
        /** now, the plainText array was decrypted. so we can handle it as usual **/
        p = new SshPacketV2(new byte[]{plainText[offset++],plainText[offset++],plainText[offset++],plainText[offset++],plainText[offset++]});
        for(int i = 0 ; i < p.getPayload_length() ; i++){
            p.addPayload((byte)plainText[offset++]);
        }
        p.setMsgcode((int)p.getPayload(0));
        p.resetOffset();
        for(int i = 0 ; i < p .getPadding_length() ; i++){
            p.addPadding(plainText[offset++]);
        }
        return p;
    }
    
    private SshPacketV2 packetReceive() throws IOException, InterruptedException{
        
        if(crypto2 != null){
            return encryptedPacketRcv();
        }
        int[] temp = new int[5];
        for(int i = 0 ; i < temp.length ; i++){
            temp[i] = dataRcv();
        }
        SshPacketV2 p = new SshPacketV2(temp);
        for(int i = 0 ; i < p.getPayload_length() ; i++){
            p.addPayload((byte)dataRcv());
        }
        p.setMsgcode((int)p.getPayload(0));
        p.resetOffset();
        for(int i = 0 ; i < p.getPadding_length() ; i++){
            p.addPadding((byte)dataRcv());
        }
        return p;
        
    }
    
    public void sendChannelData(String data) throws IOException{
        SshPacketV2 p = new SshPacketV2();
        p.setMsgcode(SSH_MSG_CHANNEL_DATA);
        p.pushInt32(0);
        p.pushString(data);
        sendPacket(p);
    }
    
    public boolean connectionAlive(){
        if(is != null && os != null){
            return true;
        }else{
            return false;
        }
    }
    boolean check = false;
    /** @param boolean
     *         true if there are some packets need to be received, and vice versa. **/
    private boolean handleSshPacketV2(SshPacketV2 p) throws SSHConnectionException, IOException, Exception{
        
        //** for human-reading used. **//
        if(check){
            System.out.print("string: ");
            System.out.print(new String(p.getPayload()));
            Integer ix = new Integer(1);
            System.out.print("\r\nHEX: ");
            for(int i = 0 ; i < p.getPayload().length ; i++){
                System.out.print(ix.toHexString((int)p.getPayload(i))+ " ");
            }
            System.out.print("\r\n");
            check = false;
        }
        // END //
        switch(p.getMsgcode()){
            case SSH_MSG_SERVICE_ACCEPT:
                authenticate2();
                return true;
            case SSH_MSG_USERAUTH_SUCCESS:
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_CHANNEL_OPEN);
                p.pushString("session");
                p.pushInt32(0);
                p.pushInt32(0x100000);
                p.pushInt32(0x4000);
                sendPacket(p);
                
                return true;
            case SSH_MSG_CHANNEL_EOF:
                return true;
            case SSH_MSG_CHANNEL_REQUEST:
                p.popByte(); //msg code
                p.popInt32();
                
                if(p.popString().equals("exit-status")){
                    
                }
                return true;
            case SSH_MSG_CHANNEL_CLOSE:
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_CHANNEL_CLOSE);
                sendPacket(p);
                
                
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_DISCONNECT);
                return handleSshPacketV2(p);
            case SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
                p.popByte(); //msg code
                p.popInt32(); //local id
                int remoteId = p.popInt32();
                // other useless information was droped.
                
                // open pty
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_CHANNEL_REQUEST);
                p.pushInt32(remoteId);
                p.pushString("pty-req");
                p.pushByte((byte)0);   // reply or not
                p.pushString("vt100");
                p.pushInt32(0);  // width
                p.pushInt32(0); // height
                p.pushInt32(0);
                p.pushInt32(0);
                p.pushString("");
                sendPacket(p);
                
                // open shell
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_CHANNEL_REQUEST);
                p.pushInt32(remoteId);
                p.pushString("shell");
                p.pushByte((byte)0);
                sendPacket(p);
                return true;
                
            case SSH_MSG_CHANNEL_WINDOW_ADJUST:
                terminal.inputLock();
                return true;
            case SSH_MSG_CHANNEL_DATA:
                p.popByte(); // msg code;
                p.popInt32(); //local id;
                String data = p.popString();
                buf.pushChars(data.toCharArray());
                buf.clearBuffer();
                return true;
            case SSH_MSG_USERAUTH_INFO_REQUEST:
                /**
                 * p.popByte(); // msg_code
                 * String name = p.popString();
                 * String instruction = p.popString();
                 * System.out.println("name and instruction : " + name + " " + instruction);
                 * p.popString(); // language tags
                 * int num_prompt = p.popInt32();
                 * String[] prompt = new String[num_prompt];
                 * byte[] echo = new byte[num_prompt];
                 * for(int i = 0 ; i < num_prompt ; i++){
                 * prompt[i] = p.popString();
                 * echo[i] = p.popByte();
                 * System.out.println("i = " + i + " " + prompt[i] + " " + echo[i]);
                 * }**/
                return false;
            case SSH_MSG_USERAUTH_FAILURE:
                synchronized(this){
                    terminal = GUI.getCurrentTerminal();
                    terminal.setSshIOinstance(this);
                    terminal.pwdReEnter(info);
                    try {
                        wait();
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
                authenticate2();
                return true;
            case SSH_MSG_KEXINIT:
                AlgAnalysis alg = new AlgAnalysis(p);
                byte[] I_S = p.getPayload();
                
                
                print("Algorithms negotiating............");
                
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_KEXINIT);
                Random r = new Random();
                
                /** generate cookie (16 byte) **/
                for(int i = 0 ; i < 16 ; i ++){
                    p.pushByte((byte)r.nextInt());
                }
                p.pushString("diffie-hellman-group1-sha1");
                p.pushString("ssh-dss");
                p.pushString("3des-cbc");
                p.pushString("3des-cbc");
                p.pushString("hmac-sha1");
                p.pushString("hmac-sha1");
                p.pushString("none");
                p.pushString("none");
                p.pushString("");
                p.pushString("");
                p.pushByte((byte)0);
                p.pushInt32(0);
                byte[] I_C = add20(p.getPayload());
                sendPacket(p);
                
                
                /** DHKex init **/
                dhkex = alg.initDHKex();
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_KEXDH_INIT);
                p.pushMpint(dhkex.getE());
                sendPacket(p);
                
                dhkex.setI_C(I_C);
                dhkex.setI_S(I_S);
                dhkex.setV_C(idstr_c.trim().getBytes());
                dhkex.setV_S(idstr.trim().getBytes());
                
                return true;
                
            case SSH_MSG_KEXDH_REPLY:
                dhkex.newPacket(p);
                if(dhkex.dhkex_reply()){
                    print("OK \r\n" + "finger print: " + fingerprint(dhkex.getK_S()) + "\r\n");
                    return true;
                }else{
                    throw new SSHConnectionException("Signature check failed");
                }
            case SSH_MSG_DISCONNECT:
                print((char)0x1B + "[mYour connection to the server \r\nhas been terminated"+(char)0x1B+"[m");
                
                is.close();
                os.close();
                sc.close();
                
                is = null;
                os = null;
                sc = null;
                this.interrupt();
                return false;
            case SSH_MSG_NEWKEYS:
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_NEWKEYS);
                sendPacket(p);
                
                updateKeys();
                
                p = new SshPacketV2();
                p.setMsgcode(SSH_MSG_SERVICE_REQUEST);
                p.pushString("ssh-userauth");
                
                sendPacket(p);
                return true;
        }
        return true;
        
    }
    private void authenticate2() throws IOException{
        SshPacketV2 p = new SshPacketV2();
        p.setMsgcode(SSH_MSG_USERAUTH_REQUEST);
        p.pushString(info.getId());
        p.pushString("ssh-connection");
        if(authMode == -1){
            // publicKey
            
            
        }else if(authMode == 0){
            // password
            p.pushString("password");
            p.pushByte((byte)0);
            p.pushString(info.getPwd());
            sendPacket(p);
            return;
            
        }else{
            //keyboard-interactive
            
        }
    }
    public void terminateConnection(){
        try {
            this.sendChannelData("exit\r\n");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    private String fingerprint(byte[] host){
        MD5 md5 = new MD5();
        StringBuffer stringBuff = new StringBuffer();
        byte[] fprint = md5.digest(host);
        
        for (int i = 0; i < fprint.length; i++) {
            int j = fprint[i] & 0xff;
            String hex = Integer.toHexString(j);
            if (hex.length() == 1) {
                stringBuff.append('0');
            }
            stringBuff.append(hex);
            if (i + 1 < fprint.length)
                stringBuff.append(':');
        }
        return stringBuff.toString();
    }
    private void updateKeys(){
        print("Updating keys.........");
        
        byte[] K = dhkex.getK();
        byte[] H = dhkex.getH();
        
        SHA1 hash = new SHA1();
        
        if(session_id == null){
            session_id = new byte[H.length];
            System.arraycopy(H, 0, session_id, 0, H.length);
        }
        /*
         * Initial IV client to server: HASH (K || H || "A" || session_id)
         * Initial IV server to client: HASH (K || H || "B" || session_id)
         * Encryption key client to server: HASH (K || H || "C" || session_id)
         * Encryption key server to client: HASH (K || H || "D" || session_id)
         * Integrity key client to server: HASH (K || H || "E" || session_id)
         * Integrity key server to client: HASH (K || H || "F" || session_id)
         */
        
        SshPacketV2 pt = new SshPacketV2();
        pt.pushMpint(K);
        pt.pushBytes(H);
        pt.pushByte((byte)0x41);
        pt.pushBytes(session_id);
        byte[] _b = pt.getPayload();
        
        hash.update(_b, 0, _b.length);
        byte[] IVc2s = new byte[hash.getDigestSize()];
        hash.doFinal(IVc2s, 0);
        
        int j = _b.length - session_id.length - 1;
        
        _b[j]++;
        hash.update(_b, 0, _b.length);
        byte[] IVs2c = new byte[hash.getDigestSize()];
        hash.doFinal(IVs2c, 0);
        
        _b[j]++;
        hash.update(_b, 0, _b.length);
        byte[] Ec2s = new byte[hash.getDigestSize()];
        hash.doFinal(Ec2s, 0);
        
        _b[j]++;
        hash.update(_b, 0, _b.length);
        byte[] Es2c = new byte[hash.getDigestSize()];
        hash.doFinal(Es2c, 0);
        
        _b[j]++;
        hash.update(_b, 0, _b.length);
        byte[] MACc2s = new byte[hash.getDigestSize()];
        hash.doFinal(MACc2s, 0);
        
        _b[j]++;
        hash.update(_b, 0, _b.length);
        byte[] MACs2c = new byte[hash.getDigestSize()];
        hash.doFinal(MACs2c, 0);
        
        int keySize = 24;
        
        while (keySize > Es2c.length) {
            pt = new SshPacketV2();
            pt.pushMpint(K);
            pt.pushBytes(H);
            pt.pushBytes(Es2c);
            _b = pt.getPayload();
            
            hash.update(_b, 0, _b.length);
            byte[] tmp1 = new byte[hash.getDigestSize()];
            hash.doFinal(tmp1, 0);
            byte[] tmp2 = new byte[Es2c.length + tmp1.length];
            System.arraycopy(Es2c, 0, tmp2, 0, Es2c.length);
            System.arraycopy(tmp1, 0, tmp2, Es2c.length, tmp1.length);
            Es2c = tmp2;
        }
        while (keySize > Ec2s.length) {
            pt = new SshPacketV2();
            pt.pushMpint(K);
            pt.pushBytes(H);
            pt.pushBytes(Ec2s);
            _b = pt.getPayload();
            
            hash.update(_b, 0, _b.length);
            byte[] tmp1 = new byte[hash.getDigestSize()];
            hash.doFinal(tmp1, 0);
            byte[] tmp2 = new byte[Ec2s.length + tmp1.length];
            System.arraycopy(Ec2s, 0, tmp2, 0, Ec2s.length);
            System.arraycopy(tmp1, 0, tmp2, Ec2s.length, tmp1.length);
            Ec2s = tmp2;
        }
        crypto2 = new SshCrypto2(IVc2s, IVs2c, Ec2s, Es2c, MACc2s, MACs2c);
        print("OK\r\n");
    }
    private int dataRcv() throws IOException, InterruptedException{
        inInt = is.read();
        if(inInt == -1){
            is.close();
            os.close();
            sc.close();
            this.interrupt();
        }
        return inInt;
    }
    
    
    /** Main Loop here **/
    private void io_verCtrl(String version) throws SSHConnectionException, IOException, InterruptedException, Exception{
        boolean conti = true;
        if(version.equals("SSH-2.0")){
            conti = true;
            
            idstr_c = "SSH-2.0-skywalker_rev_21\r\n";
            os.write(idstr_c.getBytes());
            os.flush();
            
            SshPacketV2 p = new SshPacketV2();
            while(conti){
                conti = handleSshPacketV2(packetReceive());
            }
            return;
            
        }else if(version.equals("SSH-1.0")){
            while(true){
                
            }
        }else{
            throw new SSHConnectionException("Version check failed");
        }
    }
    public void sshInit() throws IllegalArgumentException, IOException, SSHConnectionException, Exception{
        String address = "socket://"+info.getAddress() + ":" + info.getPort();
        sc = (StreamConnection)Connector.open(address,Connector.READ_WRITE,false);
        os = sc.openDataOutputStream();
        is = sc.openDataInputStream();
        
        //state msg. broacasting
        print(address + "\r\n");
        
        SshPacketV2 p = new SshPacketV2();
        p.setPacket_length(255);
        int inInt = 0;
        while(inInt != 10){
            inInt = dataRcv();
            p.addPayload((byte)inInt);
        }
        idstr = new String(p.getPayload());
        
        io_verCtrl(new String(p.getPayload(0,7)));
        if(is != null || os != null || sc != null)
            disconnect();
        
    }
    private synchronized SshPacketV2 allocateOutGoingSeq(SshPacketV2 p){
        p.setSeqnr(outgoingSeq++);
        return p;
    }
    private void sendPacket( SshPacketV2 p) throws IOException{
        p = allocateOutGoingSeq(p);
        byte[] payload = p.getPayload();
        int packet_length = 4 + 1 + 1 + payload.length; // msgcode and paddingLength field
        int padding_length = 8 - (packet_length % 8);
        if(padding_length < 4){
            padding_length += 8;
        }
        packet_length += padding_length - 4;
        byte[] padding = new byte[padding_length];
        if(crypto2 != null){
            Random rnd = new Random();
            for(int i = 0 ; i < padding.length ; i++){
                padding[i] = (byte)rnd.nextInt();
            }
        }
        
        // crypted data is:
        // packet length [ payloadlen + padlen + type + data] + padding
        byte[] out = new byte[packet_length + 4]; // 4: packetLength field
        out[0] = (byte)((packet_length >> 24) & 0xFF) ;
        out[1] = (byte)((packet_length >> 16) & 0xFF);
        out[2] = (byte)((packet_length >> 8) & 0xFF);
        out[3] = (byte)((packet_length) & 0xFF) ;
        out[4] = (byte)padding_length;
        out[5] = (byte)p.getMsgcode();
        System.arraycopy(payload,0,out,6,payload.length);
        System.arraycopy(padding,0,out,6+payload.length,padding.length);
        if(crypto2 == null){
            os.write(out);
            os.flush();
            return;
        }else{
            byte[] mac = null;
            HMACSHA1 c2smac = crypto2.getSndHmac();
            if(c2smac != null){
                c2smac.update((int)p.getSeqnr());
                c2smac.update(out, 0 , out.length);
                mac = c2smac.doFinal();
            }
            out = crypto2.encrypt(out);
            byte[] encryptSend = new byte[out.length + (mac != null ? mac.length : 0)];
            System.arraycopy(out, 0, encryptSend, 0, out.length);
            if( mac != null){
                System.arraycopy(mac, 0, encryptSend, out.length, mac.length);
            }
            os.write(encryptSend);
            os.flush();
            System.gc();
            
        }
    }
    
    private void disconnect() throws IOException{
        SshPacketV2 p = new SshPacketV2();
        p.setMsgcode(1);
        p.pushInt32(11);
        sendPacket(p);
        
        is.close();
        os.close();
        sc.close();
        
        is = null;
        os = null;
        sc = null;
    }
    
    /** 將msg code(20)補在packet的header **/
    private byte[] add20(byte[] b){
        byte[] out = new byte[b.length + 1];
        out[0] = 20;
        System.arraycopy(b, 0 , out, 1, b.length);
        return out;
    }
    
    public void setGUI(GUI gui){
        this.gui = gui;
    }
}
